/*
 * GSeasonService.java
 * Created on 1 juil. 07
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens und Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.assoc.GNationalAssociation;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubCLHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubCupHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubLeagueHistoryRecord;
import com.hockeo.vhbase.data.dao.club.GClubSeasonSummary;
import com.hockeo.vhbase.data.dao.club.GClubStats;
import com.hockeo.vhbase.data.dao.club.GClubWLHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.match.GArchivedMatch;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GArchivedStandings;
import com.hockeo.vhbase.data.dao.standings.GECRStandings;
import com.hockeo.vhbase.data.dao.standings.GENRStandings;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.dao.standings.GStandings;
import com.hockeo.vhbase.data.impl.IGAssociationImpl;
import com.hockeo.vhbase.data.impl.IGContestImpl;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.seasoncycle.movemaking.GMoveMakingService;
import com.hockeo.vhbase.seasoncycle.services.GEuropeanRankingUpdater;

/**
 * <p>
 * Contains all methods that are required to execute a hockey season. The correct method
 * call order is detailed below:
 * </p>
 *
 * <p>
 * 1) At the beginning of a season:
 * </p>
 * <ol>
 * <li> {@link #startSeason()} reversed by {@link #resetSeason(boolean)}</li>
 * </ol>
 *
 * <p>
 * 2) Before each moveday:
 * </p>
 * <ol>
 * <li>{@link #prepareNextContestRounds()} reversed by
 * {@link #undoPrepareNextContestRounds()}</li>
 * <li>{@link #executeMoveday()} reversed by {@link #undoExecuteMoveday()}</li>
 * </ol>
 *
 * <p>
 * 3) At the end of the season (after the last moveday):
 * </p>
 * <ol>
 * <li>{@link #endSeason()} reversed by {@link #resetEndOfSeason()}</li>
 * <li>{@link #createNewSeason()} reversed by {@link #undoCreateNewSeason()}</li>
 * </ol>
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( readOnly = false, rollbackFor = { GException.class } )
public class GSeasonService
{
  private static Log                     d_logger = LogFactory.getLog( GSeasonService.class );
  private IGDao                          d_dao;
  private GMoveMakingService             d_moveMakingService;
  private Map<String, IGAssociationImpl> d_mapAssocImpls;
  private Map<String, IGContestImpl>     d_mapContestImpls;

  public void setDao( final IGDao dao )
  {
    d_dao = dao;
  }

  public void setAssocImpls( final Map<String, IGAssociationImpl> mapAssocImpls )
  {
    d_mapAssocImpls = mapAssocImpls;
  }

  public void setContestImpls( final Map<String, IGContestImpl> mapContestImpls )
  {
    d_mapContestImpls = mapContestImpls;
  }

  public void setMoveMakingService( final GMoveMakingService userService )
  {
    d_moveMakingService = userService;
  }

  /*
   * ================================================================================
   * Public season lifecycle methods
   * ================================================================================
   */

  /**
   * This method must be called at the very beginning of a new season. It creates all
   * matchdays (without attaching matches to them) according to the specification of the
   * contest elements in the national and internal competitions.
   *
   * @throws GDataException if there is a logic related (semantic) problem
   */
  public void startSeason() throws GDataException
  {
    // If there is no current season, an exception is thrown right away
    //
    GSeason season = d_dao.findCurrentSeason();

    if ( season.isStarted() )
      throw new IllegalStateException( "The season " + season.getId() + " has already been started." );

    d_logger.info( "Start season " + season.getId() );

    // Prepare associations
    //
    for ( GNationalAssociation assoc : d_dao.findAllEntities( GNationalAssociation.class ) )
    {
      if ( assoc.isPrepared() )
        continue;

      d_logger.info( "* Preparing association " + assoc.getId() );

      IGAssociationImpl impl = d_mapAssocImpls.get( assoc.getImplClass() );

      // Update club seedings
      //
      impl.assignClubSeedings( assoc, season );

      // Execute league promotions/relegations
      //
      impl.doLeaguePromotionsAndRelegations( assoc, season );

      // Mark association as prepared for the season
      //
      assoc.setPrepared( true );
    }

    // Prepare contests
    //
    for ( GContest contest : d_dao.findContestsForSeason( season ) )
    {
      if ( contest.isPrepared() )
        continue;

      d_logger.info( "* Preparing contest " + contest.getId() );

      IGContestImpl impl = d_mapContestImpls.get( contest.getImplClass() );

      // Create structure
      //
      impl.createStructure( season, contest );

      // Create all matchdays (without attaching matches to them)
      //
      impl.createMatchdays( contest );

      // Assign matchdays to movedays
      //
      impl.assignMatchdaysToMovedays( contest );

      // Mark contest as being ready for the season to start
      //
      contest.setPrepared( true );
    }

    // Mark first moveday as being active
    //
    d_dao.findFirstNonExecutedMoveday().setActive( true );

    // Mark current season as being started
    //
    season.markAsStarted();
  }

  /**
   * Resets a season to its initially unprepared state.
   *
   * @param fHardReset if set to <code>true</code>, all matchdays as well as associated
   *          standings and matches are deleted no matter how many movedays have been
   *          executed so far. Please use this with great care! If <code>false</code> is
   *          passed, the season is only reset if no matchday has been prepared so far.
   * @throws GDataException if there is a data related problem
   */
  public void resetSeason( boolean fHardReset ) throws GDataException
  {
    // If a soft reset is requested and there is already at least one prepared moveday,
    // we do not continue any further.
    //
    if ( !fHardReset && d_dao.findLastPreparedMoveday() != null )
      return;

    // Ok, let's delete all season dependent data
    // ==========================================

    GSeason season = d_dao.findCurrentSeason();

    d_logger.info( "Resetting season " + season.getId() );

    // Reverse end of season actions
    //
    resetEndOfSeason();

    // Delete all contest element specific end of season stats
    //
    d_logger.info( "* Deleting end of season statistics..." );
    for ( GContestElement element : d_dao.findAllEntities( GContestElement.class ) )
    {
      IGContestImpl impl = d_mapContestImpls.get( element.getContest().getImplClass() );

      // Only if the contest element is part of the last contest round, the stats need to
      // be deleted
      //
      if ( element.getContestRound().isLastRound() )
        impl.deleteEndOfSeasonStatistics( element );
    }

    // Delete structure and mark all contests as being unprepared
    //
    d_logger.info( "* Deleting all contest structures..." );
    for ( GContest contest : d_dao.findContestsForSeason( season ) )
    {
      IGContestImpl impl = d_mapContestImpls.get( contest.getImplClass() );

      impl.undoCreateStructure( season, contest );
      contest.setPrepared( false );
    }

    // Undo association specific preparations
    //
    d_logger.info( "* Undoing association specific operations..." );
    for ( GNationalAssociation assoc : d_dao.findAllEntities( GNationalAssociation.class ) )
    {
      IGAssociationImpl impl = d_mapAssocImpls.get( assoc.getImplClass() );

      impl.undoAssignClubSeedings( assoc, season );
      impl.undoLeaguePromotionsAndRelegations( assoc, season );
      assoc.setPrepared( false );
    }

    // Reset movedays
    //
    d_logger.info( "* Resetting movedays..." );
    for ( GMoveday moveday : d_dao.findAllEntities( GMoveday.class ) )
    {
      moveday.setActive( false );
      moveday.setPrepared( false );
      moveday.setMovesMade( false );
      moveday.setExecuted( false );
    }

    // Delete standings
    //
    d_logger.info( "* Deleting all standings..." );
    d_dao.deleteAll( GStandings.class );

    // Delete matches
    //
    d_logger.info( "* Deleting all matches..." );
    d_dao.deleteAll( GMatch.class );

    // Delete matchdays
    //
    d_logger.info( "* Deleting all matchdays..." );
    d_dao.deleteAll( GMatchday.class );

    // Delete all goal accounts
    //
    d_logger.info( "* Deleting all goal accounts..." );
    d_dao.deleteAll( GGoalsAccount.class );

    // Mark the season as not being started
    //
    season.reset();
  }

  /**
   * Resets the database to its initial state at the beginning of the first season.
   *
   * This method should only be used when the application is debugged and never against
   * real production game data!
   *
   * @throws GDataException
   */
  public void resetAll() throws GDataException
  {
    GSeason seasonCurrent = d_dao.findCurrentSeason();

    d_logger.info( "Resetting the database to its initial state before the first season..." );

    // Reset current season
    //
    resetSeason( true );

    // If we only have one season, we are already done!
    //
    if ( seasonCurrent.getId() == 1 )
      return;

    d_logger.info( "Resetting previous seasons..." );

    // Reset club/league affiliations if we are not in the first season
    //
    d_logger.info( "* Resetting club/league affiliations..." );
    d_dao.resetClubLeagueAffiliations();

    // Reset club seedings
    //
    d_logger.info( "* Resetting club seedings..." );
    d_dao.resetClubSeedings( null );

    // Delete all archived data
    //
    d_logger.info( "* Deleting archived data..." );
    d_dao.deleteAll( GArchivedMatch.class );
    d_dao.deleteAll( GArchivedStandings.class );

    // Delete all historic data
    //
    d_logger.info( "* Deleting history data..." );
    d_dao.deleteAll( GClubHistoryRecord.class );
    d_dao.deleteAll( GClubSeasonSummary.class );
    d_dao.deleteAll( GClubStats.class );
    d_dao.deleteAll( GContestHistoryRecord.class );
    d_dao.deleteAll( GECRStandings.class );
    d_dao.deleteAll( GENRStandings.class );

    // Make first season the only active one
    //
    d_logger.info( "* Reactivate first season..." );
    for ( GSeason season : d_dao.findAllEntities( GSeason.class ) )
    {
      season.reset();

      if ( season.getId() == 1 )
        season.setActive( true );
      else
        season.setActive( false );
    }
  }

  /**
   * This method must be called once after the season has been started and then after
   * every executed moveday. It is responsible for checking if a new contest round needs
   * to be created. If this is the case, it creates the necessary goal accounts and
   * matches for it.
   *
   * @throws GDataException if there is a logic related (semantic) problem
   */
  public void prepareNextContestRounds() throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();
    GMoveday movedayNext = d_dao.findCurrentMoveday();

    // If the moveday had already been prepared or there is no currently active moveday,
    // there is no need to continue
    //
    if ( movedayNext == null || movedayNext.isPrepared() )
      return;

    d_logger.info( "Preparing next contest rounds. Next moveday: " + movedayNext.getId() );

    // Iterate over all contests and prepare the necessary contest rounds
    //
    for ( GContest contest : d_dao.findContestsForSeason( season ) )
    {
      d_logger.info( "* for " + contest.getId() );

      IGContestImpl impl = d_mapContestImpls.get( contest.getImplClass() );
      impl.createNextContestRound( contest, season, movedayNext );
    }

    // Mark moveday as being prepared
    //
    movedayNext.setPrepared( true );
  }

  /**
   * Resets the last prepared contest round. I.e. it deletes the matches and goal accounts
   * that have been created at the last call to {@link #prepareNextContestRounds()}.
   *
   * @throws GDataException
   */
  public void undoPrepareNextContestRounds() throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();

    // Determine last prepared moveday
    //
    GMoveday movedayLastPrepared = d_dao.findLastPreparedMoveday();

    // If the last prepared moveday has already been executed, the execution needs to be
    // reversed first.
    //
    if ( movedayLastPrepared == null || movedayLastPrepared.isExecuted() )
      return;

    d_logger.info( "Undoing preparation of next contest rounds. Last prepared moveday: " + movedayLastPrepared.getId() );

    for ( GContest contest : d_dao.findContestsForSeason( season ) )
    {
      d_logger.info( "* for " + contest.getId() );

      IGContestImpl impl = d_mapContestImpls.get( contest.getImplClass() );
      impl.undoCreateNextContestRound( contest, movedayLastPrepared );
    }

    // Mark moveday as being unprepared
    //
    movedayLastPrepared.setPrepared( false );
  }

  /**
   * Executes a particular moveday which involves the following steps:
   *
   * <ul>
   * <li>Placing of goals for all computer managed clubs.</li>
   * <li>Determining NMRs for human user managed clubs.</li>
   * <li>Analysing all matches including the update of standings in round robin contests.</li>
   * </ul>
   *
   * @throws GDataException if there is a data inconsistency problem
   */
  public void executeMoveday() throws GException
  {
    long lStartTime = System.currentTimeMillis();

    // Determine current moveday
    //
    GMoveday moveday = d_dao.findCurrentMoveday();

    // If the moveday had already been executed or there is no active moveday,
    // there is no need to continue
    //
    if ( moveday == null || moveday.isExecuted() )
      return;

    d_logger.info( "Executing moveday " + moveday.getId() );

    // Make the moves
    //
    if ( !moveday.isMovesMade() )
    {
      d_moveMakingService.makeMoves( moveday );
      d_moveMakingService.syncMatchesWithComputerMoves( moveday );
      moveday.setMovesMade( true );
    }

    // In contests where multiple elements are attached to a round, round specific
    // operations may be required. Therefore we use a hash map to store all rounds that
    // are affected by this moveday (key: affected round, object: list of matchdays).
    //
    HashMap<GContestRound, ArrayList<Integer>> mapPlayedMultiElementRounds = new HashMap<GContestRound, ArrayList<Integer>>();

    // Record all contest elements where the last matchday of a round is played
    //
    ArrayList<GContestElement> listElementsLastMatchday = new ArrayList<GContestElement>();

    // Loop over all attached matchdays
    //
    d_logger.info( "* Analysing all matchdays..." );
    for ( GMatchday matchday : moveday.getMatchdays() )
    {
      GContestElement contestElement = matchday.getContestElement();
      IGContestImpl impl = d_mapContestImpls.get( contestElement.getContestRound().getContest().getImplClass() );

      d_logger.info( "* for element " + contestElement.getId() + " / matchday " + matchday.getNum() + " (ID: "
          + matchday.getId() + ")" );

      // Analyse all matches, update standings etc.
      //
      impl.analyseMatches( contestElement, matchday );

      // If all matchdays have been played, end of season statistics may have to be
      // created or updated. Therefore we save the concerned elements.
      //
      if ( matchday.isLast() )
        listElementsLastMatchday.add( contestElement );

      // Check if we have a multi-element round. If yes, save the round in the map
      //
      if ( contestElement.getContestRound().getContestElements().size() > 1 )
      {
        if ( mapPlayedMultiElementRounds.containsKey( contestElement.getContestRound() ) )
        {
          ArrayList<Integer> listMatchdays = mapPlayedMultiElementRounds.get( contestElement.getContestRound() );
          Integer nMatchdayNum = Integer.valueOf( matchday.getNum() );

          if ( !listMatchdays.contains( nMatchdayNum ) )
            listMatchdays.add( nMatchdayNum );
        }
        else
        {
          ArrayList<Integer> listMatchdays = new ArrayList<Integer>();

          listMatchdays.add( Integer.valueOf( matchday.getNum() ) );
          mapPlayedMultiElementRounds.put( contestElement.getContestRound(), listMatchdays );
        }
      }
    }

    // Give multi-element rounds a chance for post processing
    //
    for ( GContestRound round : mapPlayedMultiElementRounds.keySet() )
    {
      IGContestImpl impl = d_mapContestImpls.get( round.getContest().getImplClass() );

      for ( Integer nMatchday : mapPlayedMultiElementRounds.get( round ) )
        impl.postprocessMultiElementRound( round, nMatchday );
    }

    // Special handling of contest elements where the last matchday of a round was played.
    // This must be called once the multi-element round post-processing has been done (not
    // before because in some contests the rank types must be corrected before for example
    // club/history records are created/updated etc.).
    //
    for ( GContestElement contestElement : listElementsLastMatchday )
    {
      // Mark contest round as finished (if there is one contest element that finishes,
      // the round can be considered to be finished because all contest elements of a
      // contest round must finish on the same matchday)
      //
      contestElement.getContestRound().setPlayed( true );

      // Create end of season stats
      //
      IGContestImpl impl = d_mapContestImpls.get( contestElement.getContestRound().getContest().getImplClass() );
      impl.createEndOfSeasonStatistics( contestElement );
    }

    // Fire managers having exceeded the number of allowed NMRs
    //
    // TODO

    // Delete all moveday dependent charts (they are outdated since a new moveday has been
    // played)
    //
    d_dao.deleteMovedayDependentCharts();

    // Mark moveday as being executed and inactive
    //
    moveday.setExecuted( true );
    moveday.setActive( false );

    // Set next moveday as being active (if there is one)
    //
    GMoveday movedayNext = d_dao.findFirstNonExecutedMoveday();
    if ( movedayNext != null )
      movedayNext.setActive( true );

    d_logger.info( "Finished executing moveday in " + ( ( System.currentTimeMillis() - lStartTime ) / 1000.0 )
        + " seconds." );
  }

  /**
   * Resets the last prepared moveday. I.e. the season will be in the same state it was in
   * before the last prepared moveday. All possibly created matches, goal accounts will be
   * deleted. Match results, created standings etc. will be reset or destroyed.
   *
   * @throws GDataException if there is a data inconsistency
   */
  public void undoExecuteMoveday() throws GDataException
  {
    // Determine last prepared moveday
    //
    GMoveday moveday = d_dao.findLastPreparedMoveday();

    if ( moveday == null || !moveday.isExecuted() )
      return;

    d_logger.info( "Undoing the execution of the moveday " + moveday.getId() );

    // First, delete end of season statistics that have possibly been created and reset
    // all goal accounts to their previous state (before the moveday was executed).
    //
    d_logger.info( "Undoing matchday analysis..." );
    HashSet<String> setHandledContestElements = new HashSet<String>();
    for ( GMatchday matchday : moveday.getMatchdays() )
    {
      GContestElement element = matchday.getContestElement();
      IGContestImpl impl = d_mapContestImpls.get( element.getContest().getImplClass() );

      d_logger.info( "* for element " + element.getId() );

      // To avoid handling a contest element twice, we use a set to save the IDs of
      // all handled contest elements.
      //
      if ( !setHandledContestElements.contains( element.getId() ) )
      {
        impl.resetContestStats( moveday, element );
        impl.resetGoalAccounts( moveday, element );
        setHandledContestElements.add( element.getId() );
      }

      // Mark contest round as not started, if we reset the first matchday of the round
      //
      if ( matchday.isFirst() )
        element.getContestRound().setStarted( false );

      if ( matchday.isLast() )
      {
        impl.deleteEndOfSeasonStatistics( element );

        // Mark contest round as not finished anymore
        //
        element.getContestRound().setPlayed( false );
      }

      // Make sure that the matchday is not marked as being analysed
      //
      matchday.setAnalysed( false );
    }

    // Reset all matches and ko matches
    //
    d_logger.info( "* Resetting all matches and KO matches..." );
    d_dao.resetMatches( moveday );
    d_dao.resetKOMatches( moveday );

    // Mark matches as being unset
    //
    moveday.setMovesMade( false );

    // Delete all newly created standings
    //
    d_dao.deleteStandings( moveday );

    // Mark this moveday as not being executed anymore
    //
    moveday.setExecuted( false );

    // The current moveday needs to be marked as not being active anymore
    //
    GMoveday movedayActive = d_dao.findCurrentMoveday();
    if ( movedayActive != null )
      movedayActive.setActive( false );

    // Set reset moveday as being the current one
    //
    moveday.setActive( true );
  }

  /**
   * This method must be called once at the very end of a season. It does the following
   * things:
   *
   * <ul>
   * <li>Update the seeding of all clubs according to their results in the national
   * leagues.</li>
   * <li>Update European club standings.</li>
   * <li>Update European nation standings.</li>
   * </ul>
   *
   * @throws GDataException
   */
  public void endSeason() throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();

    if ( season.isFinished() )
      throw new IllegalStateException( "The season " + season.getId() + " is already finished." );

    d_logger.info( "Executing end of season operations for season " + season.getId() );

    // Update European rankings
    //
    d_logger.info( "* Updating international club and nation rankings..." );
    GEuropeanRankingUpdater eru = new GEuropeanRankingUpdater( d_dao, season );
    List<GNationalAssociation> listAssocs = d_dao.findAllEntities( GNationalAssociation.class );
    for ( GNationalAssociation assoc : listAssocs )
      eru.updateRankings( assoc );

    // Attribute ranks to ECR/ENR standings
    //
    eru.updateECRRanks();
    eru.updateENRRanks();

    // Create club/season summary records
    //
    d_logger.info( "* Creating club/season summary records..." );
    for ( GNationalAssociation assoc : listAssocs )
      createClubSeasonSummaries( assoc, season );

    // Update club statistics
    //
    d_logger.info( "* Updating club statistics..." );
    updateClubStatistics();

    // Load current season and mark it as being finished
    //
    season.markAsFinished();
  }

  /**
   * Resets all the changes that have been made during the last call to
   * {@link #endSeason()}.
   *
   * @throws GDataException
   */
  public void resetEndOfSeason() throws GDataException
  {
    GSeason season = d_dao.findCurrentSeason();

    d_logger.info( "Undoing end of season operations for season " + season.getId() );

    d_dao.deleteEndOfSeasonData( season );

    updateClubStatistics();

    // Remove flag indicating that the season is finished (if it was already finished)
    //
    if ( season.isFinished() )
      season.resetFinishedFlag();
  }

  /**
   * <p>
   * Marks the current season as being inactive, deletes and archives season dependent
   * data and creates a new season if none exists. If one already exists, it is just
   * marked as being active.
   * </p>
   *
   * <p>
   * The creation of a new season cannot be reversed! Be very careful and check that the
   * current season is in an error free state before calling it.
   * </p>
   *
   * @throws GDataException
   */
  public void createNewSeason() throws GDataException
  {
    int nNewSeasonId;
    GSeason seasonLast, seasonNew;

    d_logger.info( "Creating new season." );

    // Try to find last finished season
    //
    seasonLast = d_dao.findLastFinishedSeason();
    if ( seasonLast != null )
    {
      d_logger.info( "* Finishing off last season " + seasonLast.getId() );

      // Mark all movedays as being unprepared
      //
      d_logger.info( "** Resetting movedays..." );
      for ( GMoveday moveday : d_dao.findAllEntities( GMoveday.class ) )
        moveday.reset();

      // Archive/delete matches
      //
      d_logger.info( "** Archiving and deleting matches..." );
      d_dao.archiveAndDeleteMatches( seasonLast );

      // Delete all the season dependent data that is not deleted because of dependencies
      // to other (deleted) data (through cascaded removal)
      //
      d_logger.info( "** Deleting matchdays..." );
      d_dao.deleteAll( GMatchday.class );

      d_logger.info( "** Deleting goal accounts..." );
      d_dao.deleteAll( GGoalsAccount.class );

      d_logger.info( "** Deleting standings..." );
      d_dao.deleteAll( GStandings.class );

      // Mark all contests as being unprepared and delete their structures
      //
      d_logger.info( "** Resetting contest structures..." );
      for ( GContest contest : d_dao.findContestsForSeason( seasonLast ) )
      {
        d_logger.info( "   > for " + contest.getId() );

        contest.setPrepared( false );

        IGContestImpl impl = d_mapContestImpls.get( contest.getImplClass() );
        impl.deleteStructure( seasonLast, contest );
      }

      // Mark all associations as unprepared
      //
      for ( GNationalAssociation assoc : d_dao.findAllEntities( GNationalAssociation.class ) )
        assoc.setPrepared( false );

      // The season is over, delete obsolete charts
      //
      d_dao.deleteMovedayDependentCharts();

      // Deactivate last season
      //
      if ( seasonLast.isActive() )
        seasonLast.setActive( false );

      nNewSeasonId = seasonLast.getId() + 1;
    }
    else
    {
      // If there is no last finished season, there is either a season running or none
      // exists so far. If there is a season with ID 1, we are in a wrong state. Otherwise
      // we have to create the first season.
      //
      GSeason seasonFirst = d_dao.findEntity( GSeason.class, 1 );
      if ( seasonFirst != null )
        throw new IllegalStateException( "Trying to make a new season active although at least one "
            + "non-finished season exists already." );

      nNewSeasonId = 1;
    }

    // Try to get the next season (maybe it has already been created)
    //
    d_logger.info( "* Creating or retrieving next season..." );
    seasonNew = d_dao.findEntity( GSeason.class, nNewSeasonId );
    if ( seasonNew == null )
    {
      seasonNew = new GSeason( nNewSeasonId );
      d_dao.makePersistent( seasonNew );
    }

    // Mark the season as being active
    //
    seasonNew.setActive( true );
  }

  /**
   * Deletes the current season and marks the last season as the active one. This method
   * is only successful if the current season is in a "raw" state. I.e. it only has an
   * effect if no moveday has been prepared yet.
   *
   * @throws GDataException if the operation is not feasible because there is already a
   *           prepared moveday
   */
  public void undoCreateNewSeason() throws GDataException
  {
  // Don't delete the season if it's the first season!!!
  // TODO
  }

  /**
   * Creates the club/season summary lines for the given association.
   *
   * @param assoc the association for which clubs to create the records
   * @param season the season for which to create the records
   */
  private void createClubSeasonSummaries( GNationalAssociation assoc, GSeason season )
  {
    List<GClubLeagueHistoryRecord> listLeagueHist = d_dao.findClubHistoryRecords( GClubLeagueHistoryRecord.class,
        assoc, season );
    Map<GClub, GClubCupHistoryRecord> mapCupHist = d_dao.findClubHistoryRecordsAsMap( GClubCupHistoryRecord.class,
        assoc, season );
    Map<GClub, GClubCLHistoryRecord> mapCLHist = d_dao.findClubHistoryRecordsAsMap( GClubCLHistoryRecord.class, assoc,
        season );
    Map<GClub, GClubWLHistoryRecord> mapWLHist = d_dao.findClubHistoryRecordsAsMap( GClubWLHistoryRecord.class, assoc,
        season );
    Map<GClub, GECRStandings> mapECR = d_dao.findECRStandingsAsMap( season, assoc );

    for ( GClubLeagueHistoryRecord leagueHist : listLeagueHist )
    {
      GClubSeasonSummary clubSeasonSummary = new GClubSeasonSummary( leagueHist,
          mapCupHist.get( leagueHist.getClub() ), mapCLHist.get( leagueHist.getClub() ),
          mapWLHist.get( leagueHist.getClub() ), mapECR.get( leagueHist.getClub() ) );

      d_dao.makePersistent( clubSeasonSummary );
    }
  }

  /**
   * Deletes all existing club statistics objects and recreates the relevant objects based
   * on the latest available set of club history records.
   */
  private void updateClubStatistics()
  {
    // Delete all existing club statistics objects
    //
    d_dao.deleteAll( GClubStats.class );

    // Recreate new objects based on all available club/season summary objects
    //
    List<GClubSeasonSummary> listSummaries = d_dao.findClubSeasonSummariesForClubStats();
    HashMap<GClub, GClubStats> mapClubStats = new HashMap<GClub, GClubStats>();

    for ( GClubSeasonSummary summary : listSummaries )
    {
      GClubStats stats = mapClubStats.get( summary.getClub() );
      if ( stats == null )
      {
        // If we already have a stats object referenced by the club, evict it from the
        // current persistence layer session to avoid having two objects with the same
        // identifier in it
        //
        if ( summary.getClub().getStats() != null )
          d_dao.evict( summary.getClub().getStats() );

        stats = new GClubStats( summary.getClub() );
        d_dao.makePersistent( stats );
        mapClubStats.put( summary.getClub(), stats );
      }

      stats.add( summary );
    }
  }
}
